Sequelize SQL 随记


1、Relations / Associations

1.7 关系检查(Check associations)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 检查对象是否是关联对象之一:
Project.create({ /* */ }).then(function(project) {
return User.create({ /* */ }).then(function(user) {
return project.hasUser(user).then(function(result) {
// false
return project.addUser(user).then(function() {
return project.hasUser(user).then(function(result) {
// true
})
})
})
})
})
// 检查所有对象是符合预期:
// 假设已有一个 project 和两个 users
project.setUsers([user1, user2]).then(function() {
return project.hasUsers([user1]);
}).then(function(result) {
// false
return project.hasUsers([user1, user2]);
}).then(function(result) {
// true
})

1.8 外键(Foreign Keys)

Sequelize中,如果创建了两个模型之间的关联,那么相关联的外键会被自动创建:

1
2
3
4
var Task = this.sequelize.define('task', { title: Sequelize.STRING })
, User = this.sequelize.define('user', { username: Sequelize.STRING })
User.hasMany(Task)
Task.belongsTo(User)

会生成以下SQL 语句:

1
2
3
4
5
6
7
8
9
10
CREATE TABLE IF NOT EXISTS `User` (
`id` INTEGER PRIMARY KEY,
`username` VARCHAR(255)
);
CREATE TABLE IF NOT EXISTS `Task` (
`id` INTEGER PRIMARY KEY,
`title` VARCHAR(255),
`user_id` INTEGER REFERENCES `User` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
);

task 和 user之间的关系通过task表的外键user_id引入,并REFERENCES引用user表。默认情况下,引用的user 如果被删除那么user_id会被设置为NULL,并且会随user id的更新而更新。这些选项可以在建立关系时通过MonUpdate和onDelete选项修改。可选项有:

1
RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL

对于 1:1 和 1:m 的关系,默认的删除选项是SET NULL,更新选项是CASCADE。而 n:m 的关系,两者都是CASCADE。这意味着,你从 n:m 关系的任一方删除或更新数据,其所对应的关联数据也会同时被删除或更新。
添加表之间的约束意味着,使用sequelize.sync创建表时,相关的表在数据库中必须有一定创建顺序。如果Task 表引用了User,那么User 必须在Task 之前创建。有时这会导致循环引用,想象一个场景:一个文档和版本,一个文档可以有多个版本,并且为了方便,文档对它的当前版本有一个引用。

1
2
3
4
5
6
7
8
9
var Document = this.sequelize.define('document', {
author: Sequelize.STRING
})
, Version = this.sequelize.define('version', {
timestamp: Sequelize.DATE
})
Document.hasMany(Version) // 添中 document_id 到 version中
Document.belongsTo(Version, { as: 'Current', foreignKey: 'current_version_id'}) // 添加 current_version_id 到文档中

这时可能出现类似以下错误:

1
Cyclic dependency found. 'Document' is dependent of itself. Dependency Chain: Document -> Version => Document

为了解决这个问题,可以传入一个constraints: false选项:

1
2
Document.hasMany(Version)
Document.belongsTo(Version, { as: 'Current', foreignKey: 'current_version_id', constraints: false})

这时会按以下顺序将表同步到数据库中:

1
2
3
4
5
6
7
8
9
10
CREATE TABLE IF NOT EXISTS `Document` (
`id` INTEGER PRIMARY KEY,
`author` VARCHAR(255),
`current_version_id` INTEGER
);
CREATE TABLE IF NOT EXISTS `Version` (
`id` INTEGER PRIMARY KEY,
`timestamp` DATETIME,
`document_id` INTEGER REFERENCES `Document` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
);

没有约束的外键引用

有时我们想添加一个外键引用,但不想添加任何约束或关系。这种情形下,你可以在schema定义时手动添加reference属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var Series, Trainer, Video
// 在调用Trainer.hasMany(series)方法的,Series 有一个 trainer_id=Trainer.id 的外键引用
Series = sequelize.define('series', {
title: DataTypes.STRING,
sub_title: DataTypes.STRING,
description: DataTypes.TEXT,
// Set FK relationship (hasMany) with `Trainer`
trainer_id: {
type: DataTypes.INTEGER,
references: {
model: "trainers",
key: "id"
}
}
})
Trainer = sequelize.define('trainer', {
first_name: DataTypes.STRING,
last_name: DataTypes.STRING
});
// 在调用Series.hasOne(Video)后,Video has a series_id=Series.id 的外键引用……
Video = sequelize.define('video', {
title: DataTypes.STRING,
sequence: DataTypes.INTEGER,
description: DataTypes.TEXT,
// 为`Series`设置 hasOne关系
series_id: {
type: DataTypes.INTEGER,
references: {
model: Series, // 可以是一个表示表名的字符串或模型引用
key: "id"
}
}
});
Series.hasOne(Video);
Trainer.hasMany(Series);

2、Transaction

2.1、 事务的使用

Sequelize有两种使用事务的方式:

基于Promise结果链的自动提交/回滚
另一种是不自动提交和回滚,而由用户控制事务

2.1.1 受管理的事务(auto-callback)

受管理的事务会自动提交或回滚,你可以向sequelize.transaction方法传递一个回调函数来启动一个事务。

需要注意,在这种方式下传递给回调函数的transaction会返回一个promise链,在promise链中(then或catch)中并不能调用t.commit()或t.rollback()来控制事务。在这种方式下,如果使用事务的所有promise链都执行成功,则自动提交;如果其中之一执行失败,则自动回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
return sequelize.transaction(function (t) {
// 要确保所有的查询链都有return返回
return User.create({
firstName: 'Abraham',
lastName: 'Lincoln'
}, {transaction: t}).then(function (user) {
return user.setShooter({
firstName: 'John',
lastName: 'Boothe'
}, {transaction: t});
});
}).then(function (result) {
// Transaction 会自动提交
// result 是事务回调中使用promise链中执行结果
}).catch(function (err) {
// Transaction 会自动回滚
// err 是事务回调中使用promise链中的异常结果
});

抛出错误并回滚

使用受管理的事务时,不能通过手工调用的方式来提交或回滚事务。但在需要时(如验证失败),可以通过throw来抛出异常回滚事务。

1
2
3
4
5
6
7
8
9
return sequelize.transaction(function (t) {
return User.create({
firstName: 'Abraham',
lastName: 'Lincoln'
}, {transaction: t}).then(function (user) {
// 注意,虽然所有操作成功但仍会回滚
throw new Error();
});
});

自动传递事务到所有的查询中

在上面的示例中,我们通过向第二个参数中添加{ transaction: t }选项来手工传递事务。如果要自动传递事务到所有的查询中,需要安装continuation local storage(CLS)模块并在代码中创建一个命名空间实例:

1
2
var cls = require('continuation-local-storage'),
namespace = cls.createNamespace('my-very-own-namespace');

启用CLS,需要在Sequlize构造函数属性中设置命名空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Sequelize = require('sequelize');
Sequelize.cls = namespace;
new Sequelize(....);
```
注意cls必须在constructor构造函数中设置,而不能在sequlize实例中设置。
CLS的工作方式就像一个回调函数的本地线程存储。在sequlize中启用CLS后,需要在启动事务时设置transaction命名空间。在一个回调链中设置的变量时私有的,所以几个并发事务可以同时存在。
``` javascript
sequelize.transaction(function (t1) {
namespace.get('transaction') === t1; // true
});
sequelize.transaction(function (t2) {
namespace.get('transaction') === t2; // true
});

大多数情况下,你不用通过namespace.get(‘transaction’)直接访问命名空间,因为所有的查询都会自动查找事务的命名空间。

1
2
3
4
sequelize.transaction(function (t1) {
// 启用 CLS 后,会在自动在事务中执行create 操作
return User.create({ name: 'Alice' });
});

2.1.1 不受管理的事务(then-callback)

不受管理的事务需要你强制提交或回滚,如果不进行这些操作,事务会一直保持挂起状态直到超时。

启动一个不受管理的事务,同样是调用sequelize.transaction()方法,但不传递回调函数参数(仍然可以传递选项参数)。然后可以在其返回的promisethen方法中手工控制事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
return sequelize.transaction().then(function (t) {
return User.create({
firstName: 'Homer',
lastName: 'Simpson'
}, {transaction: t}).then(function (user) {
return user.addSibling({
firstName: 'Lisa',
lastName: 'Simpson'
}, {transaction: t});
}).then(function () {
return t.commit();
}).catch(function (err) {
return t.rollback();
});
});